﻿/****************************************************************************************************
	Copyright (C) 2010 RapidWebDev Organization (http://rapidwebdev.org)
	Author: Eunge, Legal Name: Jian Liu, Email: eunge.liu@RapidWebDev.org

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.UI;
using BaoJianSoft.Common.CodeDom;
using BaoJianSoft.ExtensionModel.Properties;
using Spring.Objects;
using System.Xml.Serialization;
using BaoJianSoft.Common;

namespace BaoJianSoft.ExtensionModel.Statistic
{
	/// <summary>
	/// Api to analyze statistic report and management analyzing fields.
	/// </summary>
	public class StatisticApi : IStatisticApi
	{
		/// <summary>
		/// Property Name - "Summary".
		/// </summary>
		public const string STATISTIC_PROPERTY_NAME = "Statistic";

		/// <summary>
		/// Property Name - "RowHeaderText".
		/// </summary>
		public const string ROW_HEADER_TEXT_PROPERTY_NAME = "Name";

		private IMetadataApi metadataApi;
		private Dictionary<string, CandidateStatisticPropertyCollection> candidateStatisticPropertyCollections = new Dictionary<string, CandidateStatisticPropertyCollection>();

		/// <summary>
		/// Construct StatisticApi by specified configured candidate statistic property configuration files.
		/// </summary>
		/// <param name="metadataApi"></param>
		/// <param name="candidateStatisticPropertyConfigurationFileNames"></param>
		public StatisticApi(IMetadataApi metadataApi, IEnumerable<string> candidateStatisticPropertyConfigurationFileNames)
		{
			if (candidateStatisticPropertyConfigurationFileNames == null || candidateStatisticPropertyConfigurationFileNames.Count() == 0) return;

			XmlSerializer serializer = new XmlSerializer(typeof(CandidateStatisticPropertyCollection));
			foreach (string candidateStatisticPropertyConfigurationFileName in candidateStatisticPropertyConfigurationFileNames)
			{
				string configurationFileName = Kit.ToAbsolutePath(candidateStatisticPropertyConfigurationFileName);
				if (!File.Exists(configurationFileName))
					throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The candidate statistic property configuration file \"{0}\" to StatisticApi is not found.", candidateStatisticPropertyConfigurationFileName));

				try
				{
					using (FileStream configurationStream = new FileStream(configurationFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
					{
						CandidateStatisticPropertyCollection candidateStatisticPropertyCollection = serializer.Deserialize(configurationStream) as CandidateStatisticPropertyCollection;
						this.candidateStatisticPropertyCollections[candidateStatisticPropertyCollection.Name] = candidateStatisticPropertyCollection;
					}
				}
				catch (Exception exp)
				{
					string errorMessage = string.Format(CultureInfo.InvariantCulture, "The specified candidate statistic property configuration file \"{0}\" to StatisticApi is invalid format.", candidateStatisticPropertyConfigurationFileName);
					Logger.Instance(this).Error(errorMessage, exp);
					throw;
				}
			}

			this.metadataApi = metadataApi;
		}

		/// <summary>
		/// Create statistic report for data source by specified definition.
		/// </summary>
		/// <param name="statisticDefinition"></param>
		/// <param name="dataSource"></param>
		/// <returns></returns>
		public IEnumerable CreateStatisticReport(StatisticDefinition statisticDefinition, IList dataSource)
		{
			IDictionary<IStatisticFilter, IList> statisticDataByFilter = this.AnalyzeStatisticDataByFilter(statisticDefinition, dataSource);

			IDictionary<PropertyDefinition, StatisticFilter> propertyDefinitions4ColumnFilter = BuildPropertyDefinitions(statisticDefinition);
			IDictionary<object, StatisticFilter> returnValues4RowFilter = ConstructStatisticRecords(statisticDefinition, propertyDefinitions4ColumnFilter.Keys);

			IObjectWrapper objectWrapper = null;
			bool setRowHeaderText = false;
			foreach (PropertyDefinition propertyDefinition in propertyDefinitions4ColumnFilter.Keys)
			{
				StatisticFilter columnStatisticFilter = propertyDefinitions4ColumnFilter[propertyDefinition];
				// summary column
				if (columnStatisticFilter == null || columnStatisticFilter is AggregateStatisticFilter) continue;

				int summary4Column = 0;
				object summaryRowInstance = null;
				string propertyName = ClassDecorator.FilterIllegalCharacters(columnStatisticFilter.PropertyName);
				IList statisticDataByColumnFilter = statisticDataByFilter[columnStatisticFilter];

				// foreach to compute column&row cells excluded summary cells.
				foreach (object returnValue in returnValues4RowFilter.Keys)
				{
					if (objectWrapper == null)
						objectWrapper = new ObjectWrapper(returnValue.GetType());

					objectWrapper.WrappedInstance = returnValue;

					StatisticFilter rowStatisticFilter = returnValues4RowFilter[returnValue];
					// summary row (last row)
					if (rowStatisticFilter == null)
					{
						summaryRowInstance = returnValue;
						continue;
					}

					if (rowStatisticFilter is AggregateStatisticFilter) continue;

					// set row header text.
					if (!setRowHeaderText)
						objectWrapper.SetPropertyValue(ROW_HEADER_TEXT_PROPERTY_NAME, rowStatisticFilter.HeaderText);

					// calculate how many records matching both column and row filters.
					IList statisticDataByRowFilter = statisticDataByFilter[rowStatisticFilter];
					int matchedRecordCount = statisticDataByColumnFilter.Cast<object>().Intersect(statisticDataByRowFilter.Cast<object>()).Count();

					// set statistic back to statistic instance.
					objectWrapper.SetPropertyValue(propertyDefinition.NewPropertyName, matchedRecordCount);

					summary4Column += matchedRecordCount;
				}

				// set summary cells inner of the last row.
				objectWrapper.WrappedInstance = summaryRowInstance;
				objectWrapper.SetPropertyValue(propertyDefinition.NewPropertyName, summary4Column);
				objectWrapper.SetPropertyValue(ROW_HEADER_TEXT_PROPERTY_NAME, Resources.Summary);
				setRowHeaderText = true;
			}

			// compute aggregate/summary column cells
			for (int rowIndex = 0; rowIndex < returnValues4RowFilter.Keys.Count; rowIndex++)
			{
				objectWrapper.WrappedInstance = returnValues4RowFilter.Keys.ElementAt(rowIndex);

				IEnumerable<PropertyDefinition> propertyDefinitions = from kvp in propertyDefinitions4ColumnFilter
																	  where kvp.Value != null && !(kvp.Value is AggregateStatisticFilter)
																	  select kvp.Key;

				IEnumerable<KeyValuePair<PropertyDefinition, AggregateStatisticFilter>> propertyDefinitionAndAggregateStatisticFilterPairs = from kvp in propertyDefinitions4ColumnFilter
																																			 where kvp.Value != null && kvp.Value is AggregateStatisticFilter
																																			 select new KeyValuePair<PropertyDefinition, AggregateStatisticFilter>(kvp.Key, kvp.Value as AggregateStatisticFilter);

				#region Compute summary column

				int summaryNumber = 0;
				for (int columnIndex = 0; columnIndex < propertyDefinitions.Count(); columnIndex++)
				{
					string propertyName = propertyDefinitions.ElementAt(columnIndex).NewPropertyName;
					summaryNumber += (int)objectWrapper.GetPropertyValue(propertyName);
				}

				objectWrapper.SetPropertyValue(STATISTIC_PROPERTY_NAME, summaryNumber);

				#endregion

				#region Compute aggregate columns

				foreach (KeyValuePair<PropertyDefinition, AggregateStatisticFilter> propertyDefinitionAndAggregateStatisticFilterPair in propertyDefinitionAndAggregateStatisticFilterPairs)
				{
					AggregateStatisticFilter aggregateStatisticFilter = propertyDefinitionAndAggregateStatisticFilterPair.Value;
					if (aggregateStatisticFilter.ReferencedFilter != null && aggregateStatisticFilter.ReferencedFilter.Length > 0)
					{
						int aggregateNumber = 0;
						foreach (AggregateReferencedFilter aggregateReferencedFilter in aggregateStatisticFilter.ReferencedFilter)
						{
							StatisticFilter statisticFilter = statisticDefinition.Columns.FirstOrDefault(filter => filter.HeaderText == aggregateReferencedFilter.HeaderText);

							if (statisticFilter == null) continue;
							string newPropertyName = GetStatisticPropertyName(statisticFilter);
							PropertyDefinition propertyDefinition = propertyDefinitions.FirstOrDefault(p => p.NewPropertyName == newPropertyName);
							if (propertyDefinition != null)
								aggregateNumber += (int)objectWrapper.GetPropertyValue(propertyDefinition.NewPropertyName);
						}

						string aggregatePropertyName = GetStatisticPropertyName(aggregateStatisticFilter);
						objectWrapper.SetPropertyValue(aggregatePropertyName, aggregateNumber);
					}
				}

				#endregion
			}

			return returnValues4RowFilter.Keys;
		}

		/// <summary>
		/// Resolve all candidate statistic properties by name.
		/// </summary>
		/// <param name="candidateStatisticPropertyCollectionName"></param>
		/// <returns></returns>
		public IEnumerable<CandidateStatisticProperty> ResolveAllCandidateStatisticProperties(string candidateStatisticPropertyCollectionName)
		{
			return this.ResolveAllCandidateStatisticProperties(candidateStatisticPropertyCollectionName, null);
		}

		/// <summary>
		/// Resolve all candidate statistic properties by configuration name union with extension properties of specified extension type.
		/// </summary>
		/// <param name="candidateStatisticPropertyCollectionName"></param>
		/// <param name="extensionTypeIds"></param>
		/// <returns></returns>
		public IEnumerable<CandidateStatisticProperty> ResolveAllCandidateStatisticProperties(string candidateStatisticPropertyCollectionName, IEnumerable<Guid> extensionTypeIds)
		{
			if (!this.candidateStatisticPropertyCollections.ContainsKey(candidateStatisticPropertyCollectionName))
				throw new InvalidProgramException(string.Format(CultureInfo.InvariantCulture, "The specified candidate statistic property collection name \"{0}\" is not configured to StatisticApi.", candidateStatisticPropertyCollectionName));

			CandidateStatisticPropertyCollection candidateStatisticPropertyCollection = this.candidateStatisticPropertyCollections[candidateStatisticPropertyCollectionName];
			List<CandidateStatisticProperty> candidateStatisticProperties = new List<CandidateStatisticProperty>(candidateStatisticPropertyCollection.Property);

			if (extensionTypeIds == null || extensionTypeIds.Count() == 0) return candidateStatisticProperties;

			foreach (Guid extensionTypeId in extensionTypeIds)
			{
				IEnumerable<IFieldMetadata> fieldMetadataEnumerable = this.metadataApi.GetFields(extensionTypeId);
				if (fieldMetadataEnumerable == null || fieldMetadataEnumerable.Count() == 0) continue;

				foreach (IFieldMetadata fieldMetadata in fieldMetadataEnumerable)
				{
					CandidateStatisticProperty candidateStatisticProperty = new CandidateStatisticProperty
					{
						PropertyLabel = fieldMetadata.Name,
						PropertyName = string.Format(CultureInfo.InvariantCulture, @"[""{0}""]", fieldMetadata.Name),
						PropertyType = ResolveExtensionFieldType(fieldMetadata.Type),
						Algorithm = ResolveStatisticFilterAlgorithm(fieldMetadata.Type)
					};

					if (fieldMetadata.Type == FieldType.Enumeration)
					{
						EnumerationFieldMetadata selectionFieldMetadata = fieldMetadata as EnumerationFieldMetadata;
						if (selectionFieldMetadata.Items != null && selectionFieldMetadata.Items.Length > 0)
						{
							candidateStatisticProperty.Item = new CandidateStatisticPropertyStaticDataSource
							{
								Item = selectionFieldMetadata.Items.Select(item => new StatisticItem { Text = item.Name, Value = item.Value }).ToArray()
							};
						}
					}

					candidateStatisticProperties.Add(candidateStatisticProperty);
				}
			}

			return candidateStatisticProperties;
		}

		#region Create Statistic Report

		private IDictionary<IStatisticFilter, IList> AnalyzeStatisticDataByFilter(StatisticDefinition statisticDefinition, IList dataSource)
		{
			List<IStatisticFilter> allStatisticFilters = new List<IStatisticFilter>();
			allStatisticFilters.AddRange(statisticDefinition.Columns);
			allStatisticFilters.AddRange(statisticDefinition.Rows);

			Dictionary<IStatisticFilter, IList> results = new Dictionary<IStatisticFilter, IList>();
			foreach (IStatisticFilter statisticFilter in allStatisticFilters)
			{
				results[statisticFilter] = new ArrayList();
				string propertyName = ClassDecorator.FilterIllegalCharacters(statisticFilter.PropertyName);
				foreach (object target in dataSource)
				{
					try
					{
						object value = DataBinder.Eval(target, propertyName);
						if (statisticFilter.IsMatch(value))
							results[statisticFilter].Add(target);
					}
					catch
					{
						// the target doesn't match the statistic filter if DataBinder.Eval throws an exception.
					}
				}
			}

			return results;
		}

		private static IDictionary<object, StatisticFilter> ConstructStatisticRecords(StatisticDefinition statisticDefinition, IEnumerable<PropertyDefinition> propertyDefinitions)
		{
			Type newType = ClassDecorator.CreateDecorateType(GetStatisticTypeName(statisticDefinition), propertyDefinitions);

			Dictionary<object, StatisticFilter> records = new Dictionary<object, StatisticFilter>();
			foreach (StatisticFilter statisticFilter in statisticDefinition.Rows.OrderBy(filter => filter.Ordinal))
				records.Add(Activator.CreateInstance(newType), statisticFilter);

			records.Add(Activator.CreateInstance(newType), null);
			return records;
		}

		private static IDictionary<PropertyDefinition, StatisticFilter> BuildPropertyDefinitions(StatisticDefinition statisticDefinition)
		{
			Dictionary<PropertyDefinition, StatisticFilter> propertyDefinitions = new Dictionary<PropertyDefinition, StatisticFilter>();
			propertyDefinitions.Add(new PropertyDefinition(typeof(string)) { NewPropertyName = ROW_HEADER_TEXT_PROPERTY_NAME }, null);

			foreach (StatisticFilter statisticFilter in statisticDefinition.Columns.OrderBy(filter => filter.Ordinal))
			{
				string propertyName = GetStatisticPropertyName(statisticFilter);
				propertyDefinitions.Add(new PropertyDefinition(typeof(int)) { NewPropertyName = propertyName }, statisticFilter);
			}

			propertyDefinitions.Add(new PropertyDefinition(typeof(int)) { NewPropertyName = STATISTIC_PROPERTY_NAME }, null);
			return propertyDefinitions;
		}

		private static PropertyTypes ResolveExtensionFieldType(FieldType fieldType)
		{
			switch (fieldType)
			{
				case FieldType.DateTime:
					return PropertyTypes.DateTime;
				case FieldType.Decimal:
					return PropertyTypes.Decimal;
				case FieldType.Integer:
					return PropertyTypes.Integer;
				case FieldType.Hierarchy:
				case FieldType.Enumeration:
					return PropertyTypes.Enumerable;
				case FieldType.String:
					return PropertyTypes.String;
			}

			throw new NotImplementedException();
		}

		private static CandidateStatisticPropertyAlgorithm ResolveStatisticFilterAlgorithm(FieldType fieldType)
		{
			switch (fieldType)
			{
				case FieldType.DateTime:
					return new CandidateStatisticPropertyAlgorithm { Type = StatisticFilterAlgorithms.Between };
				case FieldType.Decimal:
					return new CandidateStatisticPropertyAlgorithm { Type = StatisticFilterAlgorithms.Between };
				case FieldType.Integer:
					return new CandidateStatisticPropertyAlgorithm { Type = StatisticFilterAlgorithms.Between };
				case FieldType.Hierarchy:
				case FieldType.Enumeration:
					return new CandidateStatisticPropertyAlgorithm { Type = StatisticFilterAlgorithms.Contains };
				case FieldType.String:
					return new CandidateStatisticPropertyAlgorithm { Type = StatisticFilterAlgorithms.Equals };
			}

			throw new NotImplementedException();
		}

		private static string GetStatisticTypeName(StatisticDefinition statisticDefinition)
		{
			string typeName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", statisticDefinition.Name, statisticDefinition.Id);
			return ClassDecorator.FilterIllegalCharacters(typeName);
		}

		/// <summary>
		/// Get property name of the dynamic object for the statistic filter.
		/// </summary>
		/// <param name="statisticFilter"></param>
		/// <returns></returns>
		internal static string GetStatisticPropertyName(StatisticFilter statisticFilter)
		{
			string propertyName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", statisticFilter.PropertyName, statisticFilter.HeaderText.GetHashCode());
			return ClassDecorator.FilterIllegalCharacters(propertyName);
		}

		#endregion

		/// <summary>
		/// Transform candidate statistic property to statistic filter.
		/// </summary>
		/// <param name="candidateStatisticProperty">candidate statistic property</param>
		/// <param name="headerText">statistic filter header text</param>
		/// <param name="ordinal">statistic filter ordinal</param>
		/// <returns></returns>
		public StatisticFilter TransformToStatisticFilter(CandidateStatisticProperty candidateStatisticProperty, string headerText, int ordinal)
		{
			StatisticFilter statisticFilter = CreateStatisticFilter(candidateStatisticProperty);

			statisticFilter.PropertyLabel = candidateStatisticProperty.PropertyLabel;
			statisticFilter.PropertyName = candidateStatisticProperty.PropertyName;
			statisticFilter.PropertyType = candidateStatisticProperty.PropertyType;
			statisticFilter.HeaderText = headerText;
			statisticFilter.Ordinal = ordinal;

			return statisticFilter;
		}

		private static StatisticFilter CreateStatisticFilter(CandidateStatisticProperty candidateStatisticProperty)
		{
			if (candidateStatisticProperty.Algorithm != null && candidateStatisticProperty.Algorithm.Type == StatisticFilterAlgorithms.Callback)
				return new CallbackStatisticFilter { CallbackType = candidateStatisticProperty.Algorithm.Value };

			if (candidateStatisticProperty.Algorithm != null && candidateStatisticProperty.Algorithm.Type == StatisticFilterAlgorithms.Aggregate)
				return new AggregateStatisticFilter();

			StatisticFilter statisticFilter = null;
			switch (candidateStatisticProperty.PropertyType)
			{
				case PropertyTypes.DateTime:
				case PropertyTypes.Decimal:
				case PropertyTypes.Integer:
					statisticFilter = new BetweenStatisticFilter();
					break;

				case PropertyTypes.String:
					if (candidateStatisticProperty.Algorithm == null || candidateStatisticProperty.Algorithm.Type == StatisticFilterAlgorithms.Equals)
						statisticFilter = new EqualsStatisticFilter();
					else if (candidateStatisticProperty.Algorithm.Type == StatisticFilterAlgorithms.Contains)
						statisticFilter = new ContainsStatisticFilter();
					else if (candidateStatisticProperty.Algorithm.Type == StatisticFilterAlgorithms.Contained)
						statisticFilter = new ContainedStatisticFilter();

					break;

				case PropertyTypes.Enumerable:
					statisticFilter = new ContainsStatisticFilter();
					break;
			}

			if (statisticFilter == null)
			{
				string algorithm = candidateStatisticProperty.Algorithm == null ? "Not Specified" : candidateStatisticProperty.Algorithm.Type.ToString();
				throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Cannot resolve correct statistic filter from property type \"{0}\" and algorithm \"{1}\".", candidateStatisticProperty.PropertyType, algorithm));
			}

			return statisticFilter;
		}
	}
}
